/*******************************************************************************
 * Copyright (c) 2009-2011 Luaj.org. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/
package org.luaj.lib.jse;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.luaj.LuaBoolean;
import org.luaj.LuaDouble;
import org.luaj.LuaFunction;
import org.luaj.LuaInteger;
import org.luaj.LuaString;
import org.luaj.LuaTable;
import org.luaj.LuaValue;
import org.luaj.Varargs;


/**
 * Helper class to coerce values from lua to Java within the luajava library.
 * <p>
 * This class is primarily used by the {@link org.luaj.lib.jse.LuajavaLib},
 * but can also be used directly when working with Java/lua bindings.
 * <p>
 * To coerce to specific Java values, generally the {@code toType()} methods
 * on {@link LuaValue} may be used:
 * <ul>
 * <li>{@link LuaValue#toboolean()}</li>
 * <li>{@link LuaValue#tobyte()}</li>
 * <li>{@link LuaValue#tochar()}</li>
 * <li>{@link LuaValue#toshort()}</li>
 * <li>{@link LuaValue#toint()}</li>
 * <li>{@link LuaValue#tofloat()}</li>
 * <li>{@link LuaValue#todouble()}</li>
 * <li>{@link LuaValue#tojstring()}</li>
 * <li>{@link LuaValue#touserdata()}</li>
 * <li>{@link LuaValue#touserdata(Class)}</li>
 * </ul>
 * <p>
 * For data in lua tables, the various methods on {@link LuaTable} can be used directly
 * to convert data to something more useful.
 *
 * @see org.luaj.lib.jse.LuajavaLib
 * @see CoerceJavaToLua
 */
@SuppressWarnings("ALL")
public class CoerceLuaToJava {

    static int SCORE_NULL_VALUE = 0x10;
    static int SCORE_NUM_WRONG_TYPE = 0x20;
    static int SCORE_INT_WRONG_TYPE = 0x80;
    static int SCORE_WRONG_TYPE = 0x100;
    static int SCORE_UNCOERCIBLE = 0x10000;

    static interface Coercion {
        public int score(LuaValue value);

        public Object coerce(LuaValue value);
    }

    ;

    /**
     * Coerce a LuaValue value to a specified java class
     *
     * @param value LuaValue to coerce
     * @param clazz Class to coerce into
     * @return Object of type clazz (or a subclass) with the corresponding value.
     */
    public static Object coerce(LuaValue value, Class clazz) {
        return getCoercion(clazz).coerce(value);
    }

    public static Object arrayCoerce(LuaValue value, Class clazz) {
        return new ArrayCoercion(clazz).coerce(value);
    }

    static final Map COERCIONS = Collections.synchronizedMap(new HashMap());

    static final class BoolCoercion implements Coercion {
        public String toString() {
            return "BoolCoercion()";
        }

        public int score(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TBOOLEAN:
                    return 0;
            }
            return 32;
        }

        public Object coerce(LuaValue value) {
            return value.toboolean() ? Boolean.TRUE : Boolean.FALSE;
        }
    }

    static final class NumericCoercion implements Coercion {
        static final int TARGET_TYPE_BYTE = 0;
        static final int TARGET_TYPE_CHAR = 1;
        static final int TARGET_TYPE_SHORT = 2;
        static final int TARGET_TYPE_INT = 3;
        static final int TARGET_TYPE_LONG = 4;
        static final int TARGET_TYPE_FLOAT = 5;
        static final int TARGET_TYPE_DOUBLE = 6;
        static final String[] TYPE_NAMES = {"byte", "char", "short", "int", "long", "float", "double"};
        final int targetType;

        public String toString() {
            return "NumericCoercion(" + TYPE_NAMES[targetType] + ")";
        }

        NumericCoercion(int targetType) {
            this.targetType = targetType;
        }

        public int score(LuaValue value) {
            int fromStringPenalty = 0;
            if (value.type() == LuaValue.TSTRING) {
                value = value.tonumber();
                if (value.isnil()) {
                    return SCORE_UNCOERCIBLE;
                }
                fromStringPenalty = 4;
            }
            if (value.isint()) {
                switch (targetType) {
                    case TARGET_TYPE_BYTE: {
                        int i = value.toint();
                        return fromStringPenalty + ((i == (byte) i) ? 0 : SCORE_WRONG_TYPE);
                    }
                    case TARGET_TYPE_CHAR: {
                        int i = value.toint();
                        return fromStringPenalty + ((i == (byte) i) ? 1 : (i == (char) i) ? 0 : SCORE_WRONG_TYPE);
                    }
                    case TARGET_TYPE_SHORT: {
                        int i = value.toint();
                        return fromStringPenalty +
                                ((i == (byte) i) ? 1 : (i == (short) i) ? 0 : SCORE_WRONG_TYPE);
                    }
                    case TARGET_TYPE_INT: {
                        long i = value.tolong();
                        return fromStringPenalty +
                                ((i == (byte) i) ? 2 : ((i == (char) i) || (i == (short) i)) ? 1 : (i == (int) i) ? 0 : SCORE_WRONG_TYPE);
                    }
                    case TARGET_TYPE_LONG: {
                        long i = value.tolong();
                        return fromStringPenalty +
                                ((i == (byte) i) ? 3 : ((i == (char) i) || (i == (short) i)) ? 2 : (i == (int) i) ? 1 : 0);
                    }
                    case TARGET_TYPE_FLOAT:
                        return fromStringPenalty + SCORE_INT_WRONG_TYPE;
                    case TARGET_TYPE_DOUBLE:
                        return fromStringPenalty + SCORE_INT_WRONG_TYPE;
                    default:
                        return SCORE_WRONG_TYPE;
                }
            } else if (value.isnumber()) {
                switch (targetType) {
                    case TARGET_TYPE_BYTE: {
                        double d = value.todouble();
                        return fromStringPenalty + ((d == (byte) d) ? 0 : SCORE_WRONG_TYPE) + SCORE_NUM_WRONG_TYPE;
                    }
                    case TARGET_TYPE_CHAR: {
                        double d = value.todouble();
                        return fromStringPenalty + ((d == (byte) d) ? 1 : (d == (char) d) ? 0 : SCORE_WRONG_TYPE) + SCORE_NUM_WRONG_TYPE;
                    }
                    case TARGET_TYPE_SHORT: {
                        double d = value.todouble();
                        return fromStringPenalty +
                                ((d == (byte) d) ? 1 : (d == (short) d) ? 0 : SCORE_WRONG_TYPE) + SCORE_NUM_WRONG_TYPE;
                    }
                    case TARGET_TYPE_INT: {
                        double d = value.todouble();
                        return fromStringPenalty +
                                ((d == (byte) d) ? 2 : ((d == (char) d) || (d == (short) d)) ? 1 : (d == (int) d) ? 0 : SCORE_WRONG_TYPE) + SCORE_NUM_WRONG_TYPE;
                    }
                    case TARGET_TYPE_LONG: {
                        double d = value.todouble();
                        return fromStringPenalty + ((d == (long) d) ? 0 : SCORE_WRONG_TYPE) + SCORE_NUM_WRONG_TYPE;
                    }
                    case TARGET_TYPE_FLOAT: {
                        double d = value.todouble();
                        return fromStringPenalty + ((d == (float) d) ? 0 : SCORE_WRONG_TYPE);
                    }
                    case TARGET_TYPE_DOUBLE: {
                        double d = value.todouble();
                        return fromStringPenalty + (((d == (long) d) || (d == (float) d)) ? 1 : 0);
                    }
                    default:
                        return SCORE_WRONG_TYPE;
                }
            } else if (value.isuserdata()) {
                Class<? extends Object> cls = value.touserdata().getClass();
                switch (targetType) {
                    case TARGET_TYPE_BYTE: {
                        return cls == Byte.class || cls == Byte.TYPE ? 0 : SCORE_UNCOERCIBLE;
                    }
                    case TARGET_TYPE_CHAR: {
                        return cls == Character.class || cls == Character.TYPE ? 0 : SCORE_UNCOERCIBLE;
                    }
                    case TARGET_TYPE_SHORT: {
                        return cls == Short.class || cls == Short.TYPE ? 0 : SCORE_UNCOERCIBLE;
                    }
                    case TARGET_TYPE_INT: {
                        return cls == Integer.class || cls == Integer.TYPE ? 0 : SCORE_UNCOERCIBLE;
                    }
                    case TARGET_TYPE_LONG: {
                        return cls == Long.class || cls == Long.TYPE ? 0 : SCORE_UNCOERCIBLE;
                    }
                    case TARGET_TYPE_FLOAT: {
                        return cls == Float.class || cls == Float.TYPE ? 0 : SCORE_UNCOERCIBLE;
                    }
                    case TARGET_TYPE_DOUBLE: {
                        return cls == Double.class || cls == Double.TYPE ? 0 : SCORE_UNCOERCIBLE;
                    }
                    default:
                        return SCORE_UNCOERCIBLE;
                }
            } else {
                return SCORE_UNCOERCIBLE;
            }
        }

        public Object coerce(LuaValue value) {
            if (value.isuserdata()) {
                Number n = value.touserdata(Number.class);
                switch (targetType) {
                    case TARGET_TYPE_BYTE:
                        return n.byteValue();
                    case TARGET_TYPE_CHAR:
                        return (char) n.intValue();
                    case TARGET_TYPE_SHORT:
                        return n.shortValue();
                    case TARGET_TYPE_INT:
                        return n.intValue();
                    case TARGET_TYPE_LONG:
                        return n.longValue();
                    case TARGET_TYPE_FLOAT:
                        return n.floatValue();
                    case TARGET_TYPE_DOUBLE:
                        return n.doubleValue();
                    default:
                        return null;
                }
            }

            switch (targetType) {
                case TARGET_TYPE_BYTE:
                    return new Byte((byte) value.toint());
                case TARGET_TYPE_CHAR:
                    return new Character((char) value.toint());
                case TARGET_TYPE_SHORT:
                    return new Short((short) value.toint());
                case TARGET_TYPE_INT:
                    return new Integer((int) value.toint());
                case TARGET_TYPE_LONG:
                    return new Long((long) value.tolong());
                case TARGET_TYPE_FLOAT:
                    return new Float((float) value.todouble());
                case TARGET_TYPE_DOUBLE:
                    return new Double((double) value.todouble());
                default:
                    return null;
            }
        }
    }

    static final class StringCoercion implements Coercion {
        public static final int TARGET_TYPE_STRING = 0;
        public static final int TARGET_TYPE_BYTES = 1;
        final int targetType;

        public StringCoercion(int targetType) {
            this.targetType = targetType;
        }

        public String toString() {
            return "StringCoercion(" + (targetType == TARGET_TYPE_STRING ? "String" : "byte[]") + ")";
        }

        public int score(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TSTRING:
                    return 0;
                case LuaValue.TNIL:
                    return SCORE_NULL_VALUE;
                case LuaValue.TUSERDATA:
                    return value.touserdata() instanceof String ? 0 : SCORE_UNCOERCIBLE;
                default:
                    return SCORE_UNCOERCIBLE;
            }
        }

        public Object coerce(LuaValue value) {
            if (value.isnil())
                return null;
            if (targetType == TARGET_TYPE_STRING)
                return value.tojstring();
            LuaString s = value.checkstring();
            byte[] b = new byte[s.m_length];
            s.copyInto(0, b, 0, b.length);
            return b;
        }
    }

    static final class ArrayCoercion implements Coercion {
        final Class componentType;
        final Coercion componentCoercion;

        public ArrayCoercion(Class componentType) {
            this.componentType = componentType;
            this.componentCoercion = getCoercion(componentType);
        }

        public String toString() {
            return "ArrayCoercion(" + componentType.getName() + ")";
        }

        public int score(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TTABLE:
                    return value.length() == 0 ? 0 : check(value);
                case LuaValue.TUSERDATA:
                    return inheritanceLevels(componentType, value.touserdata().getClass().getComponentType());
                case LuaValue.TNIL:
                    return SCORE_NULL_VALUE;
                default:
                    return SCORE_UNCOERCIBLE;
            }
        }

        private int check(LuaValue value) {
            int n = 0;
            int len = value.length();
            int s = 1;
            if (len > 10)
                s = len / 10;
            for (int i = 0; i < len; i += s) {
                int r = componentCoercion.score(value.get(i + 1));
                if (r > n)
                    n = r;
                if (r == SCORE_WRONG_TYPE)
                    break;
            }
            return n;
        }

        public Object coerce(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TTABLE: {
                    int n = value.length();
                    Object a = Array.newInstance(componentType, n);
                    for (int i = 0; i < n; i++)
                        Array.set(a, i, componentCoercion.coerce(value.get(i + 1)));
                    return a;
                }
                case LuaValue.TUSERDATA:
                    return value.touserdata();
                case LuaValue.TNIL:
                    return null;
                default:
                    return null;
            }

        }
    }

    static final class CollectionCoercion implements Coercion {
        final Class componentType;
        final Coercion componentCoercion;

        public CollectionCoercion(Class componentType) {
            this.componentType = componentType;
            this.componentCoercion = new ObjectCoercion(componentType);
        }

        public String toString() {
            return "CollectionCoercion(" + componentType.getName() + ")";
        }

        public int score(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TTABLE:
                    return 10;
                case LuaValue.TUSERDATA:
                    return inheritanceLevels(componentType, value.touserdata().getClass());
                case LuaValue.TNIL:
                    return SCORE_NULL_VALUE;
                default:
                    return SCORE_UNCOERCIBLE;
            }
        }

        public Object coerce(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TTABLE: {
                    try {
                        Collection<Object> list;
                        if (componentType.isInterface())
                            list = new ArrayList<>();
                        else
                            list = (Collection<Object>) componentType.newInstance();

                        int n = value.length();
                        for (int i = 0; i < n; i++)
                            list.add(componentCoercion.coerce(value.get(i + 1)));
                        return list;
                    } catch (IllegalAccessException e) {

                            e.printStackTrace();
                    } catch (InstantiationException e) {

                            e.printStackTrace();
                    }

                }
                case LuaValue.TUSERDATA:
                    return value.touserdata();
                case LuaValue.TNIL:
                    return null;
                default:
                    return null;
            }
        }
    }

    static final class MapCoercion implements Coercion {
        final Class componentType;
        final Coercion componentCoercion;

        public MapCoercion(Class componentType) {
            this.componentType = componentType;
            this.componentCoercion = new ObjectCoercion(componentType);
        }

        public String toString() {
            return "MapCoercion(" + componentType.getName() + ")";
        }

        public int score(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TTABLE:
                    return 10;
                case LuaValue.TUSERDATA:
                    return inheritanceLevels(componentType, value.touserdata().getClass());
                case LuaValue.TNIL:
                    return SCORE_NULL_VALUE;
                default:
                    return SCORE_UNCOERCIBLE;
            }
        }

        public Object coerce(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TTABLE: {
                    try {
                        Map<Object, Object> map;
                        if (componentType.equals(Map.class))
                            map = new HashMap<>();
                        else
                            map = (Map<Object, Object>) componentType.newInstance();

                        Varargs ret = value.next(LuaValue.NIL);
                        while (ret != LuaValue.NIL) {
                            LuaValue k = ret.arg1();
                            map.put(componentCoercion.coerce(k), componentCoercion.coerce(ret.arg(2)));
                            ret = value.next(k);
                        }
                        return map;
                    } catch (IllegalAccessException e) {

                            e.printStackTrace();
                    } catch (InstantiationException e) {

                            e.printStackTrace();
                    }
                }
                case LuaValue.TUSERDATA:
                    return value.touserdata();
                case LuaValue.TNIL:
                    return null;
                default:
                    return null;
            }

        }
    }

    static final class InterFaceCoercion implements Coercion {
        final Class componentType;
        final Coercion componentCoercion;

        public InterFaceCoercion(Class componentType) {
            this.componentType = componentType;
            this.componentCoercion = new ObjectCoercion(componentType);
        }

        public String toString() {
            return "InterFaceCoercion(" + componentType.getName() + ")";
        }

        public int score(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TTABLE:
                case LuaValue.TFUNCTION:
                    return SCORE_WRONG_TYPE;
                case LuaValue.TUSERDATA:
                    return inheritanceLevels(componentType, value.touserdata().getClass());
                case LuaValue.TNIL:
                    return SCORE_NULL_VALUE;
                default:
                    return SCORE_UNCOERCIBLE;
            }
        }

        public Object coerce(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TTABLE: {
                    return LuajavaLib.createProxy(componentType, value);
                }
                case LuaValue.TUSERDATA:
                    return value.touserdata();
                case LuaValue.TNIL:
                    return null;
                default:
                    return null;
            }
        }
    }

    /**
     * Determine levels of inheritance between a base class and a subclass
     *
     * @param baseclass base class to look for
     * @param subclass  class from which to start looking
     * @return number of inheritance levels between subclass and baseclass,
     * or SCORE_UNCOERCIBLE if not a subclass
     */
    static final int inheritanceLevels(Class baseclass, Class subclass) {
        if (subclass == null)
            return SCORE_UNCOERCIBLE;
        if (baseclass == subclass)
            return 0;
        int min = Math.min(SCORE_UNCOERCIBLE, inheritanceLevels(baseclass, subclass.getSuperclass()) + 1);
        Class[] ifaces = subclass.getInterfaces();
        for (int i = 0; i < ifaces.length; i++)
            min = Math.min(min, inheritanceLevels(baseclass, ifaces[i]) + 1);
        return min;
    }

    static final class ObjectCoercion implements Coercion {
        final Class targetType;

        ObjectCoercion(Class targetType) {
            this.targetType = targetType;
        }

        public String toString() {
            return "ObjectCoercion(" + targetType.getName() + ")";
        }

        public int score(LuaValue value) {
            if (LuaValue.class.isAssignableFrom(targetType)) {
                return inheritanceLevels(targetType, value.getClass());
            }
            switch (value.type()) {
                case LuaValue.TNUMBER:
                    return inheritanceLevels(targetType, value.isint() ? Integer.class : Double.class);
                case LuaValue.TBOOLEAN:
                    return inheritanceLevels(targetType, Boolean.class);
                case LuaValue.TSTRING:
                    return inheritanceLevels(targetType, String.class);
                case LuaValue.TUSERDATA:
                    return inheritanceLevels(targetType, value.touserdata().getClass());
                case LuaValue.TTABLE:
                    if (targetType.isInterface())
                        return SCORE_WRONG_TYPE;
                    return inheritanceLevels(targetType, LuaTable.class);
                case LuaValue.TFUNCTION:
                    if (targetType.isInterface())
                        return SCORE_WRONG_TYPE;
                    return inheritanceLevels(targetType, LuaFunction.class);
                case LuaValue.TNIL:
                    return SCORE_NULL_VALUE;
                default:
                    return inheritanceLevels(targetType, value.getClass());
            }
        }

        public Object coerce(LuaValue value) {
            if (LuaValue.class.isAssignableFrom(targetType))
                return value;
            switch (value.type()) {
                case LuaValue.TNUMBER:
                    return value.isint() ? (Object) new Integer(value.toint()) : (Object) new Double(value.todouble());
                case LuaValue.TBOOLEAN:
                    return value.toboolean() ? Boolean.TRUE : Boolean.FALSE;
                case LuaValue.TSTRING:
                    return value.tojstring();
                case LuaValue.TUSERDATA:
                    return value.optuserdata(targetType, null);
                case LuaValue.TFUNCTION:
                case LuaValue.TTABLE:
                    if (targetType.isInterface())
                        return LuajavaLib.createProxy(targetType, value).touserdata();
                    return value;
                case LuaValue.TNIL:
                    return null;
                default:
                    return value;
            }
        }
    }

    static {
        Coercion boolCoercion = new BoolCoercion();
        Coercion byteCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_BYTE);
        Coercion charCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_CHAR);
        Coercion shortCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_SHORT);
        Coercion intCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_INT);
        Coercion longCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_LONG);
        Coercion floatCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_FLOAT);
        Coercion doubleCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_DOUBLE);
        Coercion stringCoercion = new StringCoercion(StringCoercion.TARGET_TYPE_STRING);
        //Coercion bytesCoercion = new StringCoercion(StringCoercion.TARGET_TYPE_BYTES);

        COERCIONS.put(Boolean.TYPE, boolCoercion);
        COERCIONS.put(Boolean.class, boolCoercion);
        COERCIONS.put(Byte.TYPE, byteCoercion);
        COERCIONS.put(Byte.class, byteCoercion);
        COERCIONS.put(Character.TYPE, charCoercion);
        COERCIONS.put(Character.class, charCoercion);
        COERCIONS.put(Short.TYPE, shortCoercion);
        COERCIONS.put(Short.class, shortCoercion);
        COERCIONS.put(Integer.TYPE, intCoercion);
        COERCIONS.put(Integer.class, intCoercion);
        COERCIONS.put(Long.TYPE, longCoercion);
        COERCIONS.put(Long.class, longCoercion);
        COERCIONS.put(Float.TYPE, floatCoercion);
        COERCIONS.put(Float.class, floatCoercion);
        COERCIONS.put(Double.TYPE, doubleCoercion);
        COERCIONS.put(Double.class, doubleCoercion);
        COERCIONS.put(String.class, stringCoercion);
        //COERCIONS.put(byte[].class, bytesCoercion);
    }

    static Coercion getCoercion(Class c) {
        Coercion co = (Coercion) COERCIONS.get(c);
        if (co != null) {
            return co;
        }
        if (c.isArray()) {
            co = new ArrayCoercion(c.getComponentType());
        } else if (Map.class.isAssignableFrom(c)) {
            co = new MapCoercion(c);
        } else if (Collection.class.isAssignableFrom(c)) {
            co = new CollectionCoercion(c);
        }/* else	if ( c.isInterface() ) {
			co = new InterFaceCoercion(c);
		}*/ else {
            co = new ObjectCoercion(c);
        }
        COERCIONS.put(c, co);
        return co;
    }
}
